import java.io.*;
import java.net.*;
import java.util.Scanner;

/**
 * 聊天客户端类
 * 这个类实现了一个简单的聊天客户端，可以连接到服务器并发送/接收消息
 * 使用了Socket网络编程和对象流进行通信
 * 
 * Socket网络编程基础：
 * 1. Socket是网络上运行的两个程序之间双向通信链路的端点
 * 2. 客户端通过创建Socket对象连接到服务器
 * 3. 服务器通过ServerSocket监听客户端连接请求
 * 4. 一旦连接建立，双方可以通过输入输出流进行数据传输
 * 
 * 对象流的使用：
 * 1. ObjectOutputStream可以将Java对象序列化后发送到网络
 * 2. ObjectInputStream可以从网络接收数据并反序列化为Java对象
 * 3. 被传输的对象必须实现Serializable接口
 */
public class ChatClient {
    // 客户端Socket，用于与服务器建立连接
    // Socket是网络通信的基础，它封装了TCP连接的复杂细节
    // 通过Socket我们可以获取输入输出流来进行数据传输
    private Socket socket;
    
    // 对象输出流，用于向服务器发送对象数据
    // ObjectOutputStream是处理流，需要包装在其他字节输出流上
    // 它可以将Java对象转换为字节序列进行传输（序列化）
    private ObjectOutputStream output;
    
    // 对象输入流，用于接收服务器发送的对象数据
    // ObjectInputStream用于接收并还原通过网络传输的Java对象（反序列化）
    private ObjectInputStream input;
    
    // 连接状态标志，标识客户端是否仍然连接到服务器
    // boolean类型变量，true表示连接正常，false表示连接已断开
    // 用于控制程序的运行循环和资源释放
    private boolean isConnected;
    
    // 扫描器对象，用于从控制台读取用户输入
    // Scanner是Java 5引入的工具类，用于解析基本类型和字符串的输入
    // System.in是标准输入流，通常指向键盘输入
    private Scanner scanner;
    
    /**
     * 构造方法，初始化客户端并连接到指定的服务器
     * 构造方法在创建对象时自动调用，用于初始化对象的状态
     * 
     * @param host 服务器主机名或IP地址
     *             可以是域名如"www.example.com"或IP地址如"192.168.1.1"
     *             "localhost"是本地回环地址，指向本机(127.0.0.1)
     * @param port 服务器监听的端口号
     *             端口号是0-65535之间的整数，用于标识计算机上的特定服务
     *             0-1023是系统保留端口，建议使用1024以上的端口
     */
    public ChatClient(String host, int port) {
        try {
            // 创建Socket连接到指定的主机和端口
            // 这会尝试与服务器建立TCP连接
            // TCP是面向连接的协议，确保数据的可靠传输
            // Socket构造方法会阻塞直到连接建立或超时
            socket = new Socket(host, port);
            
            // 创建对象输出流，包装Socket的输出流
            // ObjectOutputStream允许我们发送序列化的Java对象
            // socket.getOutputStream()获取Socket的字节输出流
            // 将字节输出流包装成对象输出流，可以发送对象而不是原始字节
            output = new ObjectOutputStream(socket.getOutputStream());
            
            // 创建对象输入流，包装Socket的输入流
            // ObjectInputStream允许我们接收序列化的Java对象
            // socket.getInputStream()获取Socket的字节输入流
            // 将字节输入流包装成对象输入流，可以接收对象而不是原始字节
            input = new ObjectInputStream(socket.getInputStream());
            
            // 设置连接状态为已连接
            // isConnected变量用于控制客户端的运行状态
            // true表示客户端已成功连接到服务器
            isConnected = true;
            
            // 初始化扫描器，用于读取控制台输入
            // System.in是标准输入流，通常连接到键盘
            // Scanner可以方便地读取和解析各种类型的输入
            scanner = new Scanner(System.in);
            
            // 向用户显示连接成功的消息
            // System.out是标准输出流，通常连接到控制台
            // println方法会在输出后自动换行
            System.out.println("已连接到服务器");
        } catch (Exception e) {
            // 捕获连接过程中可能发生的异常（如网络问题、服务器未启动等）
            // Exception是所有异常的父类，可以捕获大部分异常情况
            // 在实际应用中应该添加适当的错误处理
            System.out.println("连接服务器失败: " + e.getMessage());
            e.printStackTrace(); // 打印详细的错误堆栈信息，便于调试
        }
    }
    
    /**
     * 启动客户端的主要功能
     * 创建一个线程来接收服务器消息，主线程用于发送用户输入的消息
     * 
     * 多线程编程基础：
     * 1. Java中可以通过继承Thread类或实现Runnable接口创建线程
     * 2. start()方法启动线程，会调用run()方法中的代码
     * 3. 多线程可以实现并发执行，提高程序效率
     * 4. 线程间共享数据需要注意同步问题
     */
    public void start() {
        // 创建并启动一个新线程专门用于接收服务器消息
        // 这样可以同时进行消息发送和接收，避免阻塞
        // new Thread()创建一个新的线程对象
        // this::receiveMessages是方法引用，等价于new Runnable(){public void run(){receiveMessages();}}
        // start()方法启动线程，使其进入就绪状态，等待CPU调度执行
        new Thread(this::receiveMessages).start();
        
        try {
            // 主循环：持续读取用户输入并发送给服务器
            // while循环会在isConnected为true时持续执行
            // 这是客户端的主消息发送循环
            while (isConnected) {
                // 提示用户输入消息
                // print方法输出后不换行，用户可以在同一行输入
                System.out.print("请输入消息 (输入quit退出): ");
                
                // 从控制台读取用户输入的一行文本
                // nextLine()方法会阻塞直到用户按下回车键
                // 返回用户输入的完整一行文本（不包括回车符）
                String message = scanner.nextLine();
                
                // 将用户输入的消息作为对象发送给服务器
                // writeObject方法可以发送任何可序列化的对象
                // 这里发送的是String对象，String类已经实现了Serializable接口
                output.writeObject(message);
                
                // 强制将缓冲区的数据发送出去
                // flush方法确保数据立即发送，而不是等待缓冲区满
                // 对于网络通信，及时发送数据很重要
                output.flush();
                
                // 检查用户是否输入了退出命令
                // equals方法用于比较两个字符串是否相等
                // 使用"quit".equals(message)而不是message.equals("quit")可以避免message为null时的空指针异常
                if ("quit".equals(message)) {
                    // 如果用户输入了quit，显示断开连接的消息
                    System.out.println("正在断开连接");
                    
                    // 更新连接状态为未连接
                    // 这会使while循环条件变为false，循环结束
                    isConnected = false;
                    
                    // 跳出循环，结束消息发送
                    // break语句立即终止当前循环
                    break;
                }
            }
        } catch (Exception e) {
            // 捕获在发送消息过程中可能发生的异常
            // 如网络中断、服务器关闭等
            System.out.println("发送消息时发生错误: " + e.getMessage());
            e.printStackTrace();
        }
        
        // 关闭客户端资源
        // 程序结束前必须关闭所有打开的资源，避免资源泄露
        close();
    }
    
    /**
     * 接收来自服务器的消息
     * 这个方法在单独的线程中运行，以便不阻塞用户输入
     * 
     * 线程安全说明：
     * 1. 该方法在独立线程中运行，与主线程并发执行
     * 2. 多个线程访问共享变量isConnected时需要注意同步
     * 3. 在简单场景下，boolean变量的读取通常是原子操作，可以不加同步
     */
    private void receiveMessages() {
        try {
            // 循环接收服务器发送的消息
            // while循环会在isConnected为true时持续执行
            // 这是客户端的消息接收循环
            while (isConnected) {
                // 从输入流中读取一个对象
                // readObject方法会阻塞直到有数据可读
                // 这是网络编程中常见的阻塞式IO操作
                Object obj = input.readObject();
                
                // 检查读取到的对象是否是字符串类型
                // instanceof操作符用于检查对象是否是指定类型的实例
                // 这是安全的类型检查方法，即使obj为null也不会抛出异常
                if (obj instanceof String) {
                    // 将对象转换为字符串
                    // 强制类型转换需要确保对象确实是目标类型，否则会抛出ClassCastException
                    String message = (String) obj;
                    
                    // 显示从服务器接收到的消息
                    // 在控制台输出接收到的消息，让用户看到
                    System.out.println("服务器返回: " + message);
                    
                    // 检查服务器是否发送了退出命令
                    // 同样使用"quit".equals(message)避免空指针异常
                    if ("quit".equals(message)) {
                        // 如果服务器发送了quit消息，表示服务器要关闭连接
                        System.out.println("服务器已关闭");
                        
                        // 更新连接状态为未连接
                        // 这会使消息接收循环结束
                        isConnected = false;
                        
                        // 跳出循环，停止接收消息
                        break;
                    }
                }
            }
        } catch (Exception e) {
            // 捕获在接收消息过程中可能发生的异常
            // 如网络中断、服务器意外关闭等
            // 当服务器关闭连接时，readObject()会抛出EOFException
            if (isConnected) {
                System.out.println("接收消息时发生错误: " + e.getMessage());
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 关闭客户端的所有资源
     * 包括流、Socket连接和扫描器
     * 
     * 资源管理原则：
     * 1. 所有打开的资源都必须在使用完毕后关闭
     * 2. 关闭顺序通常与打开顺序相反
     * 3. 使用try-catch确保即使发生异常也能继续关闭其他资源
     */
    public void close() {
        // 设置连接状态为未连接
        // 确保即使在关闭过程中也不会继续处理消息
        isConnected = false;
        
        try {
            // 按顺序关闭所有打开的资源
            // 先关闭扫描器
            // 检查scanner是否为null，避免关闭一个未初始化的对象
            if (scanner != null) scanner.close();
            
            // 关闭对象输入流
            // 检查input是否为null，避免关闭一个未初始化的对象
            // input流依赖于socket，应在socket关闭前关闭
            if (input != null) input.close();
            
            // 关闭对象输出流
            // 检查output是否为null，避免关闭一个未初始化的对象
            // output流依赖于socket，应在socket关闭前关闭
            if (output != null) output.close();
            
            // 关闭Socket连接
            // 检查socket是否为null且未关闭，避免重复关闭
            // isClosed()方法检查Socket是否已关闭
            if (socket != null && !socket.isClosed()) socket.close();
            
            // 向用户显示客户端已关闭的消息
            System.out.println("客户端已关闭");
        } catch (Exception e) {
            // 捕获在关闭资源过程中可能发生的异常
            System.out.println("关闭资源时发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    /**
     * 程序入口点
     * 创建ChatClient实例并启动客户端
     * 
     * Java程序执行流程：
     * 1. JVM查找并执行main方法
     * 2. main方法是程序的起点
     * 3. main方法结束时，程序可能仍在运行（如果有其他活动线程）
     * 4. 所有非守护线程结束后，JVM退出
     * 
     * @param args 命令行参数（本程序未使用）
     *             可以在运行程序时传入参数，如: java ChatClient arg1 arg2
     */
    public static void main(String[] args) {
        // 创建一个新的聊天客户端，连接到本地主机的8989端口
        // localhost表示本机，8989是服务器监听的端口号
        // 这里假设服务器在同一台机器上运行，并监听8989端口
        ChatClient client = new ChatClient("localhost", 8989);
        
        // 启动客户端，开始处理消息发送和接收
        // 调用start方法启动客户端的主功能
        client.start();
    }
}